package com.talkyun.utils;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicLong;

public class HttpRetryableUtils {
    public interface HttpTask {
        String send() throws IOException;

        void callback(String result);
    }

    public interface ExceptionHandler {
        void handle(Exception e);
    }

    private class RetryItem {
        HttpTask task;
        int retryNum;
        int doneNum;
        long lastDate;

        RetryItem(HttpTask task, int retryNum) {
            this.task = task;
            this.retryNum = retryNum;
            this.lastDate = System.currentTimeMillis();
        }

        long nextTime() {
            return lastDate + (int) (Math.pow(2, doneNum) * 1000);
        }
    }

    private static final AtomicLong id = new AtomicLong();
    private Map<Long, RetryItem> id2Item = new ConcurrentHashMap<Long, RetryItem>();
    private HttpRetryableUtils self = this;
    private ExecutorService executor;
    private ExceptionHandler handler;

    public HttpRetryableUtils() {
        this(2, null);
    }

    private HttpRetryableUtils(int threadNum, ExceptionHandler handler) {
        ExecutorBuilder builder = new ExecutorBuilder();
        this.executor = builder.prefix("http-retry-exec").minPoolSize(threadNum).build();
        this.handler = handler;

        Looper trigger = new Looper("http-retry-loop", 10) {
            @Override
            protected void loop() {
                long now = System.currentTimeMillis();
                for (Map.Entry<Long, RetryItem> entry : id2Item.entrySet()) {
                    final RetryItem item = entry.getValue();
                    if (now > item.nextTime()) {
                        // inc flag
                        item.doneNum++;
                        item.lastDate = now;
                        if (item.doneNum >= item.retryNum) {
                            id2Item.remove(entry.getKey());
                        }

                        // retry now
                        this.retry(item);
                    }
                }
            }

            void retry(final RetryItem item) {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        boolean error = false;
                        String result = null;
                        try {
                            result = item.task.send();
                        } catch (IOException e) {
                            error = true;
                            self.handleException(e);
                        }
                        if (!error) {
                            self.doCallback(item.task, result);
                        }
                    }
                });
            }
        };

        trigger.start();
    }

    public void send(HttpTask task) {
        this.send(task, 3);
    }

    public void send(HttpTask task, int retryNum) {
        boolean error = false;
        String result = null;
        try {
            result = task.send();
        } catch (IOException e) {
            error = true;
            this.handleException(e);
            this.addRetryTask(new RetryItem(task, retryNum));
        }
        if (!error) {
            this.doCallback(task, result);
        }
    }

    private void doCallback(HttpTask task, String result) {
        try {
            task.callback(result);
        } catch (Exception e) {
            this.handleException(e);
        }
    }

    private void addRetryTask(RetryItem item) {
        id2Item.put(id.incrementAndGet(), item);
    }

    private void handleException(Exception e) {
        if (handler != null) {
            try {
                handler.handle(e);
            } catch (Throwable t) {
                // ignore
            }
        }
    }
}